/*
 * Copyright 2011-2018 Blender Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "render/stats.h"
#include "render/object.h"
#include "util/util_algorithm.h"
#include "util/util_foreach.h"
#include "util/util_string.h"

CCL_NAMESPACE_BEGIN

static int kIndentNumSpaces = 2;

/* Named size entry. */

namespace {

bool namedSizeEntryComparator(const NamedSizeEntry& a, const NamedSizeEntry& b)
{
	/* We sort in descending order. */
	return a.size > b.size;
}

bool namedTimeSampleEntryComparator(const NamedNestedSampleStats& a, const NamedNestedSampleStats& b)
{
	return a.sum_samples > b.sum_samples;
}

bool namedSampleCountPairComparator(const NamedSampleCountPair& a, const NamedSampleCountPair& b)
{
	return a.samples > b.samples;
}

}  // namespace

NamedSizeEntry::NamedSizeEntry()
    : name(""),
      size(0) {
}

NamedSizeEntry::NamedSizeEntry(const string& name, size_t size)
    : name(name),
      size(size) {
}

/* Named size statistics. */

NamedSizeStats::NamedSizeStats()
    : total_size(0) {
}

void NamedSizeStats::add_entry(const NamedSizeEntry& entry) {
	total_size += entry.size;
	entries.push_back(entry);
}

string NamedSizeStats::full_report(int indent_level)
{
	const string indent(indent_level * kIndentNumSpaces, ' ');
	const string double_indent = indent + indent;
	string result = "";
	result += string_printf("%sTotal memory: %s (%s)\n",
	                        indent.c_str(),
	                        string_human_readable_size(total_size).c_str(),
	                        string_human_readable_number(total_size).c_str());
	sort(entries.begin(), entries.end(), namedSizeEntryComparator);
	foreach(const NamedSizeEntry& entry, entries) {
		result += string_printf(
		        "%s%-32s %s (%s)\n",
		        double_indent.c_str(),
		        entry.name.c_str(),
		        string_human_readable_size(entry.size).c_str(),
		        string_human_readable_number(entry.size).c_str());
	}
	return result;
}

/* Named time sample statistics. */

NamedNestedSampleStats::NamedNestedSampleStats()
 : name(""), self_samples(0), sum_samples(0)
{}

NamedNestedSampleStats::NamedNestedSampleStats(const string& name, uint64_t samples)
 : name(name), self_samples(samples), sum_samples(samples)
{}

NamedNestedSampleStats& NamedNestedSampleStats::add_entry(const string& name_, uint64_t samples_)
{
	entries.push_back(NamedNestedSampleStats(name_, samples_));
	return entries[entries.size()-1];
}

void NamedNestedSampleStats::update_sum()
{
	sum_samples = self_samples;
	foreach(NamedNestedSampleStats& entry, entries) {
		entry.update_sum();
		sum_samples += entry.sum_samples;
	}
}

string NamedNestedSampleStats::full_report(int indent_level, uint64_t total_samples)
{
	update_sum();

	if(total_samples == 0) {
		total_samples = sum_samples;
	}

	const string indent(indent_level * kIndentNumSpaces, ' ');

	const double sum_percent = 100*((double) sum_samples) / total_samples;
	const double sum_seconds = sum_samples * 0.001;
	const double self_percent = 100*((double) self_samples) / total_samples;
	const double self_seconds = self_samples * 0.001;
	string info = string_printf("%-32s: Total %3.2f%% (%.2fs), Self %3.2f%% (%.2fs)\n",
	                            name.c_str(),
	                            sum_percent,
	                            sum_seconds,
	                            self_percent,
	                            self_seconds);
	string result = indent + info;

	sort(entries.begin(), entries.end(), namedTimeSampleEntryComparator);
	foreach(NamedNestedSampleStats& entry, entries) {
		result += entry.full_report(indent_level + 1, total_samples);
	}
	return result;
}

/* Named sample count pairs. */

NamedSampleCountPair::NamedSampleCountPair(const ustring& name, uint64_t samples, uint64_t hits)
 : name(name), samples(samples), hits(hits)
{}

NamedSampleCountStats::NamedSampleCountStats()
{}

void NamedSampleCountStats::add(const ustring& name, uint64_t samples, uint64_t hits)
{
	entry_map::iterator entry = entries.find(name);
	if(entry != entries.end()) {
		entry->second.samples += samples;
		entry->second.hits += hits;
		return;
	}
	entries.emplace(name, NamedSampleCountPair(name, samples, hits));
}

string NamedSampleCountStats::full_report(int indent_level)
{
	const string indent(indent_level * kIndentNumSpaces, ' ');

	vector<NamedSampleCountPair> sorted_entries;
	sorted_entries.reserve(entries.size());

	uint64_t total_hits = 0, total_samples = 0;
	foreach(entry_map::const_reference entry, entries) {
		const NamedSampleCountPair &pair = entry.second;

		total_hits += pair.hits;
		total_samples += pair.samples;

		sorted_entries.push_back(pair);
	}
	const double avg_samples_per_hit = ((double) total_samples) / total_hits;

	sort(sorted_entries.begin(), sorted_entries.end(), namedSampleCountPairComparator);

	string result = "";
	foreach(const NamedSampleCountPair& entry, sorted_entries) {
		const double seconds = entry.samples * 0.001;
		const double relative = ((double) entry.samples) / (entry.hits * avg_samples_per_hit);

		result += indent + string_printf("%-32s: %.2fs (Relative cost: %.2f)\n",
		                                 entry.name.c_str(),
		                                 seconds,
		                                 relative);
	}
	return result;
}

/* Mesh statistics. */

MeshStats::MeshStats() {
}

string MeshStats::full_report(int indent_level)
{
	const string indent(indent_level * kIndentNumSpaces, ' ');
	string result = "";
	result += indent + "Geometry:\n" + geometry.full_report(indent_level + 1);
	return result;
}

/* Image statistics. */

ImageStats::ImageStats() {
}

string ImageStats::full_report(int indent_level)
{
	const string indent(indent_level * kIndentNumSpaces, ' ');
	string result = "";
	result += indent + "Textures:\n" + textures.full_report(indent_level + 1);
	return result;
}

/* Overall statistics. */

RenderStats::RenderStats() {
	has_profiling = false;
}

void RenderStats::collect_profiling(Scene *scene, Profiler& prof)
{
	has_profiling = true;

	kernel = NamedNestedSampleStats("Total render time", prof.get_event(PROFILING_UNKNOWN));

	kernel.add_entry("Ray setup", prof.get_event(PROFILING_RAY_SETUP));
	kernel.add_entry("Result writing", prof.get_event(PROFILING_WRITE_RESULT));

	NamedNestedSampleStats &integrator = kernel.add_entry("Path integration", prof.get_event(PROFILING_PATH_INTEGRATE));
	integrator.add_entry("Scene intersection", prof.get_event(PROFILING_SCENE_INTERSECT));
	integrator.add_entry("Indirect emission", prof.get_event(PROFILING_INDIRECT_EMISSION));
	integrator.add_entry("Volumes", prof.get_event(PROFILING_VOLUME));

	NamedNestedSampleStats &shading = integrator.add_entry("Shading", 0);
	shading.add_entry("Shader Setup", prof.get_event(PROFILING_SHADER_SETUP));
	shading.add_entry("Shader Eval", prof.get_event(PROFILING_SHADER_EVAL));
	shading.add_entry("Shader Apply", prof.get_event(PROFILING_SHADER_APPLY));
	shading.add_entry("Ambient Occlusion", prof.get_event(PROFILING_AO));
	shading.add_entry("Subsurface", prof.get_event(PROFILING_SUBSURFACE));

	integrator.add_entry("Connect Light", prof.get_event(PROFILING_CONNECT_LIGHT));
	integrator.add_entry("Surface Bounce", prof.get_event(PROFILING_SURFACE_BOUNCE));

	NamedNestedSampleStats &intersection = kernel.add_entry("Intersection", 0);
	intersection.add_entry("Full Intersection", prof.get_event(PROFILING_INTERSECT));
	intersection.add_entry("Local Intersection", prof.get_event(PROFILING_INTERSECT_LOCAL));
	intersection.add_entry("Shadow All Intersection", prof.get_event(PROFILING_INTERSECT_SHADOW_ALL));
	intersection.add_entry("Volume Intersection", prof.get_event(PROFILING_INTERSECT_VOLUME));
	intersection.add_entry("Volume All Intersection", prof.get_event(PROFILING_INTERSECT_VOLUME_ALL));

	NamedNestedSampleStats &closure = kernel.add_entry("Closures", 0);
	closure.add_entry("Surface Closure Evaluation", prof.get_event(PROFILING_CLOSURE_EVAL));
	closure.add_entry("Surface Closure Sampling", prof.get_event(PROFILING_CLOSURE_SAMPLE));
	closure.add_entry("Volume Closure Evaluation", prof.get_event(PROFILING_CLOSURE_VOLUME_EVAL));
	closure.add_entry("Volume Closure Sampling", prof.get_event(PROFILING_CLOSURE_VOLUME_SAMPLE));

	NamedNestedSampleStats &denoising = kernel.add_entry("Denoising", prof.get_event(PROFILING_DENOISING));
	denoising.add_entry("Construct Transform", prof.get_event(PROFILING_DENOISING_CONSTRUCT_TRANSFORM));
	denoising.add_entry("Reconstruct", prof.get_event(PROFILING_DENOISING_RECONSTRUCT));

	NamedNestedSampleStats &prefilter = denoising.add_entry("Prefiltering", 0);
	prefilter.add_entry("Divide Shadow", prof.get_event(PROFILING_DENOISING_DIVIDE_SHADOW));
	prefilter.add_entry("Non-Local means", prof.get_event(PROFILING_DENOISING_NON_LOCAL_MEANS));
	prefilter.add_entry("Get Feature", prof.get_event(PROFILING_DENOISING_GET_FEATURE));
	prefilter.add_entry("Detect Outliers", prof.get_event(PROFILING_DENOISING_DETECT_OUTLIERS));
	prefilter.add_entry("Combine Halves", prof.get_event(PROFILING_DENOISING_COMBINE_HALVES));

	shaders.entries.clear();
	foreach(Shader *shader, scene->shaders) {
		uint64_t samples, hits;
		if(prof.get_shader(shader->id, samples, hits)) {
			shaders.add(shader->name, samples, hits);
		}
	}

	objects.entries.clear();
	foreach(Object *object, scene->objects) {
		uint64_t samples, hits;
		if(prof.get_object(object->get_device_index(), samples, hits)) {
			objects.add(object->name, samples, hits);
		}
	}
}

string RenderStats::full_report()
{
	string result = "";
	result += "Mesh statistics:\n" + mesh.full_report(1);
	result += "Image statistics:\n" + image.full_report(1);
	if(has_profiling) {
		result += "Kernel statistics:\n" + kernel.full_report(1);
		result += "Shader statistics:\n" + shaders.full_report(1);
		result += "Object statistics:\n" + objects.full_report(1);
	}
	else {
		result += "Profiling information not available (only works with CPU rendering)";
	}
	return result;
}

CCL_NAMESPACE_END
